package cn.iocoder.yudao.module.infra.service.file;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;

/**
 * 文件 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Slf4j
public class FileServiceImpl implements FileService {

    private LoadingCache<Long, FileDO> fileDOLoadingCache;
    @Resource
    private FileConfigService fileConfigService;

    @Resource
    private FileMapper fileMapper;

    @Resource
    private FileConfigProperties configProperties;

    @PostConstruct
    public void init() {
        fileDOLoadingCache = CacheBuilder.newBuilder().maximumSize(5000).expireAfterAccess(Duration.ofHours(2)).build(new CacheLoader<>() {
            @NotNull
            @Override
            public FileDO load(Long key) throws Exception {
                FileDO fileDO = fileMapper.selectById(key);
                if (fileDO == null) {
                    log.warn("文件不存在,fileId: {}", key);
                    fileDO = new FileDO();
                }
                return fileDO;
            }
        });
    }

    @Override
    public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
        return fileMapper.selectPage(pageReqVO);
    }

    @Override
    public String createFile(String name, String path, byte[] content) {
        return createFile(name, path, content, null).getFileUrl();
    }

    @Override
    public void checkFileExist(Long fileId) {
        this.validateFileExists(fileId);
    }

    @Override
    @SneakyThrows
    public UploadFileResult createFile(String name, String path, byte[] content, Long configId) {
        // 计算默认的 path 名
        String type = FileTypeUtils.getMineType(content, name);
        if (StrUtil.isEmpty(path)) {
            path = FileUtils.generatePath(content, name);
        }
        // 如果 name 为空，则使用 path 填充
        if (StrUtil.isEmpty(name)) {
            name = path;
        }

        // 加密文件
        final Pair<String, byte[]> encryptRes = encryptFileContent(type, content);
        content = encryptRes.getValue();
        final String encryptKey = encryptRes.getKey();

        log.info("上传文件configId:{},type={},name = {},path = {}", configId, type, name, path);
        // 上传到文件存储器
        FileClient client;
        if (configId != null) {
            client = fileConfigService.getFileClient(configId);
        } else {
            client = fileConfigService.getMasterFileClient();
        }
        log.info("upload client = {}", client.getClass().getName());
        Assert.notNull(client, "客户端(master) 不能为空");
        String url = client.upload(content, path, type);

        // 保存到数据库
        FileDO file = new FileDO();
        file.setConfigId(client.getId());
        file.setName(name);
        file.setPath(path);
        file.setUrl(url);
        file.setType(type);
        file.setSize(content.length);
        file.setEncryptKey(encryptKey);
        fileMapper.insert(file);

        UploadFileResult uploadFileResult = new UploadFileResult();
        uploadFileResult.setFileUrl(url);
        uploadFileResult.setFileId(file.getId());
        uploadFileResult.setPath(path);
        uploadFileResult.setEncryptKey(encryptKey);
        return uploadFileResult;
    }

    @Override
    @SneakyThrows
    public UploadFileResult onlyCreateFile(String name, String path, byte[] content, Long configId) {
        // 计算默认的 path 名
        String type = FileTypeUtils.getMineType(content, name);
        if (StrUtil.isEmpty(path)) {
            path = FileUtils.generatePath(content, name);
        }
        // 如果 name 为空，则使用 path 填充
        if (StrUtil.isEmpty(name)) {
            name = path;
        }

        // 加密文件
        final Pair<String, byte[]> encryptRes = encryptFileContent(type, content);
        content = encryptRes.getValue();
        final String encryptKey = encryptRes.getKey();

        log.info("上传文件configId:{},type={},name = {},path = {}", configId, type, name, path);
        // 上传到文件存储器
        FileClient client;
        if (configId != null) {
            client = fileConfigService.getFileClient(configId);
        } else {
            client = fileConfigService.getMasterFileClient();
        }
        log.info("upload client = {}", client.getClass().getName());
        Assert.notNull(client, "客户端(master) 不能为空");
        String url = client.upload(content, path, type);

        UploadFileResult uploadFileResult = new UploadFileResult();
        uploadFileResult.setFileUrl(url);
        uploadFileResult.setFileId(null);
        uploadFileResult.setPath(path);
        uploadFileResult.setEncryptKey(encryptKey);
        return uploadFileResult;
    }

    private Collection<FileDO> getFileFromCache(Collection<Long> fileIdList) {
        try {
            ImmutableMap<Long, FileDO> allPresent = fileDOLoadingCache.getAll(fileIdList);
            List<FileDO> fileDOS = allPresent.values().stream().filter(c -> c.getId() != null).collect(Collectors.toList());
            return fileDOS;
        } catch (Exception e) {
            log.warn("查询文件失败", e);
        }
        return new ArrayList<>(1);
    }

    @Override
    public List<UploadFileResult> getFile(Collection<Long> fileIdList, Map<Long, Integer> resourceDurationSecMap) {
        if (fileIdList == null || fileIdList.isEmpty()) {
            return Collections.EMPTY_LIST;
        }

        Collection<FileDO> fileDOS = this.getFileFromCache(fileIdList);
        List<UploadFileResult> uploadFileResults = new ArrayList<>(fileDOS.size());
        for (FileDO fileDO : fileDOS) {
            UploadFileResult fileResult = new UploadFileResult();
            fileResult.setFileId(fileDO.getId());
            fileResult.setFileUrl(fileDO.getUrl());
            fileResult.setPath(fileDO.getPath());
            fileResult.setFileName(fileDO.getName());
            fileResult.setEncryptKey(fileDO.getEncryptKey());
            uploadFileResults.add(fileResult);
        }

        uploadFileResults.parallelStream().forEach(v -> {
            final Integer resourceDurationSec = resourceDurationSecMap != null ? resourceDurationSecMap.get(v.getFileId()) : null;
            v.setFileUrl(getTempDownloadUrl(v.getEncryptKey(), v.getPath(), resourceDurationSec));
        });


        return uploadFileResults;
    }

    @Override
    public UploadFileResult getFile(long fileId, Integer resourceDurationSec) {
        FileDO fileDO = this.fileDOLoadingCache.getUnchecked(fileId);
        UploadFileResult fileResult = new UploadFileResult();
        fileResult.setFileId(fileDO.getId());
        fileResult.setFileUrl(fileDO.getUrl());
        fileResult.setPath(fileDO.getPath());
        fileResult.setFileName(fileDO.getName());
        fileResult.setEncryptKey(fileDO.getEncryptKey());
        fileResult.setFileUrl(getTempDownloadUrl(fileDO.getEncryptKey(), fileResult.getPath(), resourceDurationSec));
        return fileResult;
    }

    @Override
    public Long createFile(FileCreateReqVO createReqVO) {
        FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
        fileMapper.insert(file);
        return file.getId();
    }

    @Override
    public void deleteFile(Long id) throws Exception {
        // 校验存在
        FileDO file = validateFileExists(id);
        this.fileDOLoadingCache.invalidate(id);

        // 从文件存储器中删除
        FileClient client = fileConfigService.getFileClient(file.getConfigId());
        Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
        client.delete(file.getPath());

        // 删除记录
        fileMapper.deleteById(id);
    }

    private FileDO validateFileExists(Long id) {
        FileDO fileDO = fileMapper.selectById(id);
        if (fileDO == null) {
            throw exception(FILE_NOT_EXISTS);
        }
        return fileDO;
    }

    @Override
    public byte[] getFileContent(Long configId, String path) throws Exception {
        FileClient client = fileConfigService.getFileClient(configId);
        Assert.notNull(client, "客户端({}) 不能为空", configId);
        return client.getContent(path);
    }

    @Override
    public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {
        FileClient fileClient = fileConfigService.getMasterFileClient();
        FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);
        return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,
                object -> object.setConfigId(fileClient.getId()));
    }

    @Override
    public String getPresignedObjectUrl(String path, String method, Integer expirySec) throws Exception {
        FileClient fileClient = fileConfigService.getMasterFileClient();
        return fileClient.getPresignedObjectUrl(path, method, expirySec);
    }

    @Override
    public String getTempDownloadUrl(String encryptKey, String path, Integer resourceDurationSec) {
        final Integer expirySec;
        if (resourceDurationSec == null) {
            expirySec = 60 * 30;
        } else {
            expirySec = resourceDurationSec + (60 * 30); // 额外增加 30 分钟
        }

        String downloadUrl;
        try {
            if (path.startsWith("/")) {
                path = path.substring(1);
            }
            downloadUrl = getPresignedObjectUrl(path, "GET", expirySec);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        if (StrUtil.isNotEmpty(downloadUrl) && StrUtil.isNotEmpty(encryptKey)) {
            final String iv = configProperties.getIv();
            final String secretKey = configProperties.getSecretKey();
            final AES aes = SecureUtil.aes(secretKey.getBytes(StandardCharsets.UTF_8));
            aes.setIv(iv.getBytes(StandardCharsets.UTF_8));
            final String newKey = aes.encryptBase64(encryptKey.getBytes(StandardCharsets.UTF_8));
            downloadUrl = downloadUrl + "&key=" + newKey;
        }

        return downloadUrl;
    }

    private Pair<String, byte[]> encryptFileContent(String type, byte[] plainText) {
        if (plainText == null || StrUtil.isEmpty(type)) {
            return null;
        }

        final Boolean enableEncrypt = configProperties.getEnableEncrypt();
        if (enableEncrypt == null || !enableEncrypt) {
            return new Pair<>(null, plainText);
        }

        final List<String> encryptTypes = configProperties.getEncryptTypes();
        if (CollUtil.isEmpty(encryptTypes)) {
            return new Pair<>(null, plainText);
        }

        boolean encrypt = false;
        for (String encryptType : encryptTypes) {
            if (type.startsWith(encryptType)) {
                encrypt = true;
                break;
            }
        }
        if (!encrypt) {
            return new Pair<>(null, plainText);
        }

        final String iv = configProperties.getIv();
        final String key = RandomUtil.randomString(256 / 8);

        final AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
        aes.setIv(iv.getBytes(StandardCharsets.UTF_8));
        final byte[] encryptData = aes.encrypt(plainText);
        return new Pair<>(key, encryptData);
    }

    public static void main(String[] args) {
        byte[] content = FileUtil.readBytes("/Users/edy/Desktop/audio/be2dab96-79ef-457c-8e4e-2a2898ecf352.mp3");

        FileServiceImpl impl = new FileServiceImpl();
        impl.configProperties = new FileConfigProperties();
        impl.configProperties.setEnableEncrypt(true);
        impl.configProperties.setEncryptTypes(Arrays.asList("audio"));
        impl.configProperties.setIv("lmrobot@2025");
        Pair<String, byte[]> res = impl.encryptFileContent("audio/mpeg", content);
        System.out.println(res.getKey());
        FileUtil.writeBytes(res.getValue(), "/Users/edy/Desktop/audio/encryptData.mp3");
    }
}
